At the end of this section, you should be able to:
Identify the core components of a function definition and explain their role (the function() directive, arguments, argument defaults, function body, return value)
Describe the difference between argument matching by position and by name
Write if-else, if-else if-else statements to conditionally execute code
Write your own function to carry out a repeated task
Replicate your function multiple times using map()
12.2 Functions
12.2.1 Why functions?
Getting really good at writing useful and reusable functions is one of the best ways to increase your expertise in data science. It requires a lot of practice.
If you’ve copied and pasted code 3 or more times, it’s time to write a function. Try to avoid repeating yourself.
Reducing errors: Copy + paste + modify is prone to errors (e.g., forgetting to change a variable name)
Efficiency: If you need to update code, you only need to do it one place. This allows reuse of code within and across projects.
Readability: Encapsulating code within a function with a descriptive name makes code more readable.
df<-tibble( a =rnorm(5), b =rnorm(5), c =rnorm(5), d =rnorm(5),)df|>mutate( a =(a-min(a, na.rm =TRUE))/(max(a, na.rm =TRUE)-min(a, na.rm =TRUE)), b =(b-min(a, na.rm =TRUE))/(max(b, na.rm =TRUE)-min(b, na.rm =TRUE)), c =(c-min(c, na.rm =TRUE))/(max(c, na.rm =TRUE)-min(c, na.rm =TRUE)), d =(d-min(d, na.rm =TRUE))/(max(d, na.rm =TRUE)-min(d, na.rm =TRUE)),)
# A tibble: 5 × 4
a b c d
<dbl> <dbl> <dbl> <dbl>
1 0.707 -0.204 0 0.758
2 0.252 0.796 0.229 0.115
3 0.190 0.235 0.655 0.772
4 1 0.0457 0.157 1
5 0 -0.0266 1 0
You might be able to puzzle out that this rescales each column to have a range from 0 to 1. But did you spot the mistake? (Example from R4DS, and…) When Hadley wrote the code he made an error when copying-and-pasting and forgot to change an a to a b. Preventing exactly this type of mistake is one very good reason to learn how to write functions.
The key to the work above is that we want to repeat a set of code multiple times. The code we want to replicate can be written as:
where █ represents the part of the code that changes each time the function is run.
12.2.3 Parts of a function
To create a function you need three things:
A name. Here we’ll use rescale01() because this function rescales a vector to lie between 0 and 1.
The arguments. The arguments are things that vary across calls and our analysis above tells us that we have just one. We’ll call it x because this is the conventional name for a numeric vector.
The body. The body is the code that’s repeated across all the calls.
Then you create a function by following the template:
df|>mutate( a =rescale01(a), b =rescale01(b), c =rescale01(c), d =rescale01(d),)
# A tibble: 5 × 4
a b c d
<dbl> <dbl> <dbl> <dbl>
1 0.707 0 0 0.758
2 0.252 1 0.229 0.115
3 0.190 0.439 0.655 0.772
4 1 0.250 0.157 1
5 0 0.178 1 0
12.2.4 Ordering and arguments
When calling a function, if you don’t name the arguments, R assumes that you passed them in the order defined inside the function.
my_power<-function(x, y){return(x^y)}my_power(x =2, y =3)
[1] 8
my_power(y =3, x =2)
[1] 8
my_power(2, 3)
[1] 8
my_power(3, 2)
[1] 9
12.3 Argument matching
In general, it is safest to match arguments by name and position for your peace of mind. For functions that you are very familiar with (and know the argument order), it’s okay to just use positional matching.
Error in average1(some_data): argument "remove_nas" is missing, with no default
average1(some_data, remove_nas =TRUE)
[1] 13.4
average2(some_data)
Error in average2(some_data): argument "remove_nas" is missing, with no default
average2(some_data, remove_nas =TRUE)
[1] 13.4
average3(some_data)
[1] 13.4
withoutreturn(): the function returns the last value which gets computed and isn’t stored as an object (using <-).
withreturn(): the function will return an object that is explicitly included in the return() call. (Note: if you (accidentally?) have two return() calls, the function will return the object in the first return() call.)
12.4 Control flow
Often inside functions, you will want to execute code conditionally. In a programming language, control structures are parts of the language that allow you to control what code is executed. By far the most common is the if-else if-else structure.
if(logical_condition){# some code}elseif(other_logical_condition){# some other code}else{# yet more code}
Note that inside the curly else brackets, {}, you can have additional lines of code computing objects or conditions, or you can return desired objects.
You can include as many } else if { conditions as your problem calls for.
Functions that return the same number of rows as the original data frame are good to use inside mutate() and filter(). For example, you might want to capitalize the first word of every string:
Functions that collapse into a single value will work well in the summarize() step of the pipeline. For example, you may want to calculate the coefficient of variation which is the standard deviation divided by the mean.
cv<-function(x, na.rm=FALSE){sd(x, na.rm =na.rm)/mean(x, na.rm =na.rm)}cv(runif(100, min =0, max =50))
Arguments allow us specify the inputs when we call a function
If inputs are not named when calling the function, R uses the ordering from the function definition
All arguments must be specified when calling a function
Default arguments can be specified when the function is defined
The input to a function can be a function!
12.5 Iterating functions
There will be times when you will need to iterate a function multiple times.
12.5.1purrr for functional programming
We will see the R package purrr in greater detail as we go, but for now, let’s get a hint for how it works.
We are going to focus on the map family of functions which will just get us started. Lots of other good purrr functions like pluck() and accumulate() and across() from dplyr.
Much of below is taken from a tutorial by Rebecca Barter.
The map functions are named by the output the produce. For example:
map(.x, .f) is the main mapping function and returns a list
map_df(.x, .f) returns a data frame
map_dbl(.x, .f) returns a numeric (double) vector
map_chr(.x, .f) returns a character vector
map_lgl(.x, .f) returns a logical vector
Note that the first argument is always the data object and the second object is always the function you want to iteratively apply to each element in the input object.
The input to a map function is always either a vector (like a column), a list (which can be non-rectangular), or a dataframe (like a rectangle).
A list is a way to hold things which might be very different in shape:
a_list<-list(a_number =5, a_vector =c("a", "b", "c"), a_dataframe =data.frame(a =1:3, b =c("q", "b", "z"), c =c("bananas", "are", "so very great")))print(a_list)
$a_number
[1] 5
$a_vector
[1] "a" "b" "c"
$a_dataframe
a b c
1 1 q bananas
2 2 b are
3 3 z so very great
What if we want a different type of output? If the function outputs a data frame, we can combine the values in the list using a row-bind, list_rbind().
Error in `list_rbind()`:
! Each element of `x` must be either a data frame or `NULL`.
ℹ Elements 1, 2, and 3 are not.
Darn! We get an error because the output of the add_ten() function is a scalar, not a data frame. In order to use list_rbind() we need to edit the add_ten() function.
Mostly, the tilde will be used for functions we already know but want to modify (if we don’t modify, and it has a simple name, we don’t use the tilde):